<?php

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

include CORE_PATH . "/Core/Db/FluentPDO/FluentPDO.php";

/**
 * Description of Core_Table
 *
 * @package Core
 * @author superx
 */
abstract class Core_Db_Table extends Core_Singleton implements Core_Dao_DataProviderInterface {

    protected $_pk = 'id';
    protected $_tablename;
    protected $_ori_tablename;

    protected $_connectionName = 'default';
    protected $_oriConnectionName = '';
    protected $_shardCollectionName = 'shards';

    protected $_model;

    protected function __construct() {
        parent::__construct();
        $class = get_called_class();
        if (!$this->_tablename) {
            $this->_tablename = strtolower(preg_replace('([A-Z])', '_\0', lcfirst(substr($class, 6))));
        }
        $this->_ori_tablename = $this->_tablename;
        $this->_oriConnectionName = $this->_connectionName;
        $this->_model = str_replace('Table_', 'Model_', $class);
    }

    public static function getInstance() {
        /* @var $i self */
        $i = parent::getInstance();
        //#7589
        //force switch back to default conncection
        $i->switchConnection();
        return $i;
    }

    /**
     * @return PDO
     */
    public function getConnection() {
        return Core_Db_Connection::getInstance($this->_connectionName);
    }

    public function switchConnection($name = '') {
        $this->_connectionName = $name ? $name : $this->_oriConnectionName;
        return $this;
    }


    /**
     *
     * @param mixed $key
     * @param string $method could be 'seq'|'hash', default is 'hash'
     * @return $this
     */
    public function startSharding($key, $method = 'hash') {
        $shards = Core_Config::get('db.' . $this->_shardCollectionName);
        $total = count($shards);

        switch ($method) {
            case 'hash':
            default:
                $key = abs(crc32($key));
                $target = floor(($key % 360) / (360 / $total));
                break;
        }
        $this->switchConnection("{$this->_shardCollectionName}.{$target}");
        return $this;
    }

    /**
     * @return $this
     */
    public function endSharding() {
        $this->switchConnection();
        return $this;
    }

    /**
     * @param $table_name
     * @return $this
     */
    public function changeTable($table_name) {
        $this->_tablename = $table_name;
        return $this;
    }

    /**
     * @return $this
     */
    public function restoreTable() {
        $this->_tablename = $this->_ori_tablename;
        return $this;
    }

    public function getTableName() {
        $dbname = Core_Config::get("db.{$this->_connectionName}.dbname");
        return "`{$dbname}`.`{$this->_tablename}`";
    }

    /**
     *
     * @return SelectQuery
     */
    public function getQuery() {
        return $this->getFluentPDO()->from($this->_tablename . ' ' . substr($this->_tablename, 0, 1))->asObject($this->_model);
    }

    /**
     * @return FluentPDO
     */
    public function getFluentPDO() {
        $fpdo = new FluentPDO($this->getConnection(), new FluentStructure($this->getPK()));

        $fpdo->debug = function ($q) {
            Core_Stats::$dbQueryCnt++;
            if (Core_Config::get('Core.SQLDebug')) {
                $backtrace = '';
                $query = $q->getQuery();
                $parameters = $q->getParameters();
//                $debug = '';
//                if ($parameters) {
                $debug = "# parameters: " . implode(", ", array_map('strval', $parameters)) . "\n";
//                }
                $debug .= $query;
                $pattern = '(^' . preg_quote(dirname(__FILE__)) . '(\\.php$|[/\\\\]))'; // can be static
                foreach (debug_backtrace() as $backtrace) {
                    if (isset($backtrace["file"]) && !preg_match($pattern, $backtrace["file"])) {
                        // stop on first file outside FluentPDO source codes
                        break;
                    }
                }
                $time = sprintf('%0.3f', $q->getTime() * 1000) . ' ms';
                $rows = ($q->getResult()) ? $q->getResult()->rowCount() : 0;

                Core_Log::log("# {$backtrace['file']}:{$backtrace['line']} ($time; rows = $rows)\n$debug\n\n", 'SQLDebug');
            }
        };
        return $fpdo;
    }

    public function getPK() {
        return $this->_pk;
    }

    public function __toString() {
        return $this->_tablename;
    }

    /**
     * @param $pk
     * @return Core_Model|false
     */
    public function find($pk) {
        if (!$pk) {
            return false;
        }
        return $this->getFluentPDO()->from($this->_tablename, $pk)->asObject($this->_model)->fetch();
    }

    public function findAll(array $pks) {
        if (!$pks) {
            return array();
        }
        $ret = array();
        foreach ($pks as $pk) {
            $row = $this->find($pk);
            if ($row) {
                $ret[] = $row;
            }
        }
        return $ret;
    }

    /**
     * @param array $pks
     * @return array
     */
    public function findAllBatch(array $pks) {
        if (!$pks) {
            return array();
        }
        return $this->getQuery()->whereIn($this->getPK(), $pks)->fetchAll($this->getPK());
    }

    /* *******************************************
     * implement interface
     * ***************************************** */

    /**
     * @param $data
     * @return int the last inserted id | false
     */
    public function insert($data) {
        return $this->_insert($data)->execute();
    }

    protected function quoteColName($data) {
        $ret = array();
        foreach ($data as $key => $val) {
            if (!is_array($val)) {
                if (strpos($key, '`') === false) {
                    $ret["`{$key}`"] = $val;
                } else {
                    $ret[$key] = $val;
                }
            } else {
                $ret[$key] = $this->quoteColName($val);
            }
        }
        return $ret;
    }

    /**
     * @param array $data
     * @return int
     */
    public function insertBatch(array $data) {
        return $this->_insert($data)->execute();
    }

    /**
     * @param $data
     * @return int the last inserted id
     */
    public function insertIgnore($data) {
        return $this->_insert($data)->ignore()->execute();
    }

    public function insertOnDuplicate($data, array $update) {
        return $this->_insert($data)->onDuplicateKeyUpdate($update)->execute();
    }

    /**
     * @param $data
     * @return InsertQuery
     */
    protected function _insert($data) {
        if ($data instanceof Core_Model) {
            $data = $data->toArray();
            $data = array_filter($data, function ($var) {
                return $var !== NULL;
            });
        }
        $data = $this->quoteColName($data);
        return $this->getFluentPDO()->insertInto($this->_tablename, $data);
    }

    /**
     * @param $data
     * @param $pk
     * @return int the row count | false
     */
    public function update($data, $pk) {
        $data = $this->quoteColName($data);
        return $this->getFluentPDO()->update($this->_tablename, $data, $this->_pk, $pk)->execute();
    }

    /**
     * @param $set
     * @param $where
     * @param $whereParams
     * @return \UpdateQuery
     */
    public function updateWhere($set, $where, $whereParams) {
        $args = func_get_args();
        array_unshift($args, $this->getTableName());
        return call_user_func_array(array($this->getFluentPDO(), 'update'), $args);
    }

    /**
     * @param $pk
     * @return int the row count | false
     */
    public function delete($pk) {
        return $this->getFluentPDO()->deleteFrom($this->_tablename, $this->_pk, $pk)->execute();
    }

    /**
     * @param $where
     * @param $whereParams
     * @return \DeleteQuery
     */
    public function deleteWhere($where, $whereParams) {
        $args = func_get_args();
        array_unshift($args, $this->getTableName());
        return call_user_func_array(array($this->getFluentPDO(), 'deleteFrom'), $args);
    }

}